昨天 Day 20,我們用 1-based 與 0-based 座標判斷人工 Motif 區間是否重疊。今天,假設研究團隊拿到一個變異檔案(VCF):七列記錄,其中一列寫了兩個不同的 ALT,因此拆成八個候選。研究助理能不能只挑三筆「看起來正常」的,其他全部丟掉?
不能。
好的研究流程必須回答:哪些候選通過資料品質門檻?哪些需要人工處理?哪些被排除?每一筆為什麼得到這個結果? 對通過門檻的候選,還要能記錄「人工分數來源」以及「查無分數」,而不是隨手補上 0。
這就是 Research Review Packet(研究審閱封包)。它不是病因確診書,而是讓後續研究者能複查每一步的候選與證據整理。
| 資料 | 用途 | 本篇最重要的限制 |
|---|---|---|
day21_synthetic.vcf |
七筆八欄的人工 VCF 記錄,第二列含 G,C 兩個 ALT |
沒有樣本 genotype 欄位、沒有病人資料;只是一小部分 VCF 規範 |
day21_synthetic_regions.bed |
指定兩段人工區間:chr1:[90,115) 和 [145,165) |
BED 是 0-based,左含右不含 |
day21_toy_reference.json |
chr1 由 200 個 A 組成的虛構參考序列 |
TOY_NOT_GRCH38,絕不能當 GRCh38 FASTA |
day21_synthetic_scores.json |
提供兩個人工分數 0.42、−0.17 |
不是 AVI、沒有醫療分數含義 |
VCF 的 POS 是 1-based;例如第 100 個鹼基,要以 POS−1=99 比較 BED 是否包含它。本例判斷 indel 是否進入教學區間時,也只用 VCF POS 錨點,沒有進行複雜變異的範圍比對、左對齊或正規化。
程式使用明確的第一個決策理由:
FILTER → BED → REF → indel → missing QUAL → low QUAL
這個順序是我們為教學自行訂定的簡化規則,不是臨床或 VCF 官方的通用閾值。例如:
chr1:120:A>T 的 QUAL 是 10,但位置先被人工 BED 排除,因此輸出是 OUTSIDE_TEACHING_BED,而不是 LOW_QUAL。chr1:140:G>T 的 REF 對不上人工參考序列,但同樣先在 BED 檢查被排除,輸出仍是 OUTSIDE_TEACHING_BED。chr1:150:A>AT 是尚未正規化的 indel,標為 REVIEW_REQUIRED,不能當作簡單 SNV 已通過。chr1:160:A>T 缺少 QUAL,保留 MISSING_QUAL,不擅自補零。這個差異還可以銜接 Day 22:當我們另造在 BED 內的人工低 QUAL/REF mismatch 候選時,就能各自測到 LOW_QUAL、REF_MISMATCH。
不會寫程式也沒關係。先把以下內容貼到 Google AI Studio Build Mode,做出不連後端的靜態面板,再理解每個畫面欄位在 Python 對應哪個步驟。
請用繁體中文建立「VCF → QC → 註釋 → Research Review Packet」研究教育 MVP。
所有資料必須標示 SYNTHETIC_EDUCATIONAL_DEMO,不能上傳真實病人 VCF。
使用七列、八個 ALT 候選的人工範例,呈現:
1. VCF 輸入預覽(CHROM/POS/REF/ALT/QUAL/FILTER);多 ALT 必須拆列。
2. 可見的 QC 順序 FILTER → BED → REF → indel → missing QUAL → low QUAL。
3. QC 表顯示每筆第一個決策原因:3 SELECTED、2 REVIEW_REQUIRED、3 REJECTED。
4. 第二個表格僅顯示三筆 SELECTED 的人工註釋:0.42、-0.17、null。
5. 三筆註釋都顯示 review_status=REVIEW_REQUIRED;不得將 SELECTED 解讀為高風險或致病。
6. 顯示輸入檔案來源 SHA-256 與 Evidence Class。
7. 提供 JSON 預覽和明顯的 OFFLINE_ONLY/無真實 API 提醒。
請使用完全人工資料,不做臨床診斷、不使用真實 AlphaGenome API,不要詢問 API Key。
| AI Studio 面板功能 | Python 端的實際函式 | 今天要學的技術 |
|---|---|---|
| VCF 上傳預覽 | parse_synthetic_vcf() |
TSV、多 ALT 拆分、欄位驗證 |
| 區間篩選 | in_bed() |
1-based POS 與 0-based BED |
| QC 判定 | check_qc() |
早期返回、理由優先順序 |
| 註釋與封包 | build_review_packet() |
null、結構化 JSON、人工審查狀態 |
| UI 表格 | render_html() |
不靠網路的報告展示 |
| 檔案指紋 | hash_file() |
SHA-256 協助核對內容一致性 |
Google AI Studio 產生的網頁可能使用 React 或 Node.js;不是把網頁程式直接轉成 Python。我們要看懂它呈現的資料流,再用 Python 重建離線核心。
這次故意把資料拆成五個檔案:四份人工 Fixture + 一個主 Python。這能讓初學者從「程式碼跟資料不是同一件事」開始,逐步養成可追溯、可測試的習慣。
請下載配套 ZIP 並解壓,確認 day21_offline_vcf_review_lab.py、四份人工 Fixture 都在同一目錄。下面是主 Python 檔案的完整內容(與 ZIP 中版本一致):
"""Day 21: offline-only synthetic VCF -> QC -> annotation -> review packet.
Educational example using an invented chr1 reference, BED and annotation scores.
Not a complete VCF parser, genome annotation pipeline, or medical classifier.
No SDK, HTTP, credential loading, patient data, or real API calls.
Python 3.12+ standard library only.
"""
from __future__ import annotations
import argparse
import hashlib
from html import escape
import json
import math
from pathlib import Path
import re
VCF_FIELDS = ("#CHROM", "POS", "ID", "REF", "ALT", "QUAL", "FILTER", "INFO")
ASSEMBLY = "TOY_NOT_GRCH38"
SOURCE_FLAG = "##source=SYNTHETIC_EDUCATIONAL_DEMO"
REFERENCE_FLAG = "##reference=TOY_NOT_GRCH38"
ALLELE_RE = re.compile(r"[ACGT]+\Z")
VARIANT_RE = re.compile(r"chr[0-9XYM]+:[1-9][0-9]*:[ACGT]+>[ACGT]+\Z")
QC_ORDER = ["FILTER", "BED", "REF", "INDEL", "MISSING_QUAL", "LOW_QUAL"]
def parse_synthetic_vcf(text):
"""Parse just the documented 8-column, sample-free synthetic VCF subset."""
if not isinstance(text, str) or SOURCE_FLAG not in text.splitlines() or REFERENCE_FLAG not in text.splitlines():
raise ValueError("VCF must explicitly identify its synthetic source and toy reference")
seen_header = False
rows = []
variant_ids = set()
for line_number, line in enumerate(text.splitlines(), start=1):
if not line.strip() or line.startswith("##"):
continue
if line.startswith("#"):
if seen_header or tuple(line.split("\t")) != VCF_FIELDS:
raise ValueError("Only the eight-column educational VCF header is supported")
seen_header = True
continue
if not seen_header:
raise ValueError("VCF records require the eight-column header")
cells = line.split("\t")
if len(cells) != len(VCF_FIELDS):
raise ValueError(f"Line {line_number}: expected 8 VCF fields")
chrom, pos_raw, _identifier, ref, alts, qual_raw, filt, _info = cells
if not re.fullmatch(r"chr[0-9XYM]+", chrom):
raise ValueError(f"Line {line_number}: invalid chromosome")
if not pos_raw.isdecimal() or int(pos_raw) < 1:
raise ValueError(f"Line {line_number}: invalid 1-based POS")
pos = int(pos_raw)
if ALLELE_RE.fullmatch(ref) is None or not alts:
raise ValueError(f"Line {line_number}: invalid REF/ALT")
if qual_raw == ".":
qual = None
else:
try:
qual = float(qual_raw)
except ValueError as exc:
raise ValueError(f"Line {line_number}: invalid QUAL") from exc
if not math.isfinite(qual) or qual < 0:
raise ValueError(f"Line {line_number}: QUAL must be finite and nonnegative")
if not filt or filt == ".":
raise ValueError(f"Line {line_number}: FILTER must be present")
for alt in alts.split(","):
if ALLELE_RE.fullmatch(alt) is None or alt == ref:
raise ValueError(f"Line {line_number}: invalid ALT allele")
vid = f"{chrom}:{pos}:{ref}>{alt}"
if vid in variant_ids:
raise ValueError(f"Line {line_number}: duplicate variant identifier")
variant_ids.add(vid)
rows.append({"variant_id": vid, "chrom": chrom, "pos_1based": pos,
"ref": ref, "alt": alt, "qual": qual, "filter": filt,
"source_line": line_number})
if not seen_header or not rows:
raise ValueError("Require synthetic header and at least one variant")
return rows
def load_toy_reference(text):
obj = json.loads(text)
if not isinstance(obj, dict) or obj.get("data_kind") != "SYNTHETIC_FIXTURE" or obj.get("assembly") != ASSEMBLY:
raise ValueError("Reference must be an explicit synthetic fixture")
chromosomes = obj.get("chromosomes")
if not isinstance(chromosomes, dict) or not chromosomes:
raise ValueError("Reference chromosomes object is required")
for chrom, sequence in chromosomes.items():
if not isinstance(chrom, str) or not isinstance(sequence, str) or ALLELE_RE.fullmatch(sequence) is None:
raise ValueError("Invalid toy reference sequence")
return chromosomes
def parse_toy_bed(text):
lines = [line for line in text.splitlines() if line.strip() and not line.startswith("#")]
if not text.startswith("#SYNTHETIC_FIXTURE") or not lines:
raise ValueError("BED must be an explicit synthetic fixture")
intervals = []
for line in lines:
cells = line.split("\t")
if len(cells) != 3 or not re.fullmatch(r"chr[0-9XYM]+", cells[0]):
raise ValueError("BED must have chrom, start, end")
try:
start, end = int(cells[1]), int(cells[2])
except ValueError as exc:
raise ValueError("BED coordinates must be integers") from exc
if start < 0 or end <= start:
raise ValueError("BED requires nonempty 0-based half-open intervals")
intervals.append((cells[0], start, end))
return intervals
def load_toy_scores(text):
obj = json.loads(text)
if not isinstance(obj, dict) or obj.get("data_kind") != "SYNTHETIC_FIXTURE" or obj.get("assembly") != ASSEMBLY:
raise ValueError("Scores must be an explicit synthetic fixture")
scores = obj.get("scores")
if not isinstance(scores, dict):
raise ValueError("Score mapping must be an object")
for variant, value in scores.items():
if VARIANT_RE.fullmatch(variant) is None or isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value):
raise ValueError("Invalid synthetic score key/value")
return scores
def in_bed(chrom, pos_1based, intervals):
anchor_0based = pos_1based - 1
return any(c == chrom and start <= anchor_0based < end
for c, start, end in intervals)
def check_qc(row, reference, intervals, min_qual=20.0):
"""Return first deciding QC status/reason (not all possible reasons)."""
if row["filter"] != "PASS":
return "REJECTED", "FILTER_NOT_PASS"
if not in_bed(row["chrom"], row["pos_1based"], intervals):
return "REJECTED", "OUTSIDE_TEACHING_BED"
seq = reference.get(row["chrom"], "")
start = row["pos_1based"] - 1
if seq[start:start + len(row["ref"])] != row["ref"]:
return "REJECTED", "REF_MISMATCH"
if len(row["ref"]) != 1 or len(row["alt"]) != 1:
return "REVIEW_REQUIRED", "INDEL_NOT_NORMALIZED"
if row["qual"] is None:
return "REVIEW_REQUIRED", "MISSING_QUAL"
if row["qual"] < min_qual:
return "REJECTED", "LOW_QUAL"
return "SELECTED", "TEACHING_QC_PASS"
def build_review_packet(vcf_text, reference, intervals, scores):
all_candidates = parse_synthetic_vcf(vcf_text)
qc_rows = []
review_packet = []
for row in all_candidates:
status, reason = check_qc(row, reference, intervals)
qc_rows.append({"variant_id": row["variant_id"], "qc_status": status,
"reason": reason, "qual": row["qual"],
"filter": row["filter"], "source_line": row["source_line"]})
if status == "SELECTED":
score = scores.get(row["variant_id"])
review_packet.append({"variant_id": row["variant_id"],
"synthetic_score": score,
"score_status": "MISSING_SCORE" if score is None else "SYNTHETIC_SCORE",
"review_status": "REVIEW_REQUIRED",
"evidence_class": "SYNTHETIC_FIXTURE"})
report = {
"evidence_class": "SYNTHETIC_EDUCATIONAL_DEMO",
"mode": "OFFLINE_ONLY",
"assembly": ASSEMBLY,
"candidate_count": len(all_candidates),
"selected_count": len(review_packet),
"review_required_qc_count": sum(row["qc_status"] == "REVIEW_REQUIRED" for row in qc_rows),
"rejected_count": sum(row["qc_status"] == "REJECTED" for row in qc_rows),
"missing_score_count": sum(row["synthetic_score"] is None for row in review_packet),
"qc_order": QC_ORDER,
"qc_rows": qc_rows,
"review_packet": review_packet,
"score_source": "SYNTHETIC_FIXTURE_ONLY",
"atlas_query_performed": False,
"clinical_interpretation": False,
"real_api_calls_this_run": 0,
}
if report["selected_count"] + report["review_required_qc_count"] + report["rejected_count"] != report["candidate_count"]:
raise RuntimeError("QC group counts do not match candidates")
return report
def render_html(report):
"""Self-contained static, offline teaching UI; escape ALL external strings."""
def col(value):
if value is None:
return '<span class="missing">null</span>'
return escape(str(value), quote=True)
qc = "\n".join(
'<tr><td>' + col(r["variant_id"]) + '</td><td><span class="pill">' +
col(r["qc_status"]) + '</span></td><td>' + col(r["reason"]) +
'</td><td>' + col(r["qual"]) + '</td></tr>' for r in report["qc_rows"])
packets = "\n".join(
'<tr><td>' + col(r["variant_id"]) + '</td><td>' +
col(r["synthetic_score"]) + '</td><td>' + col(r["score_status"]) +
'</td><td>' + col(r["review_status"]) + '</td></tr>' for r in report["review_packet"])
return f'''<!doctype html>
<html lang="zh-Hant"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Day 21 | Synthetic VCF Review Packet</title><style>
:root{{color-scheme:light;font-family:system-ui,"Noto Sans TC",sans-serif;color:#283348;background:#f7f5f2}}
body{{margin:0 auto;padding:28px 22px 60px;max-width:1100px}}
h1{{font-size:1.8rem;margin:0 0 6px}}.intro{{color:#536378}}.warn{{background:#fff2dd;border-left:5px solid #bd783b;padding:14px 18px;margin:20px 0;border-radius:8px}}
.stats{{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin:20px 0}}
.stat{{background:#fff;border:1px solid #e1e2e5;border-radius:14px;padding:18px}}.num{{font-size:2rem;font-weight:750;color:#285b87}}.label{{font-size:.88rem;color:#5b6982}}
section{{background:#fff;border:1px solid #e1e2e5;border-radius:14px;padding:18px;margin-top:20px;overflow-x:auto}}
table{{width:100%;border-collapse:collapse;font-size:.9rem}}th,td{{text-align:left;padding:11px;border-bottom:1px solid #eee}}th{{background:#eef2fa}}.pill{{font-size:.8rem;background:#e6edf4;border-radius:7px;padding:4px 7px}}.missing{{color:#ae3b41;font-weight:700}}
footer{{font-size:.83rem;color:#5b6982;margin-top:30px}}</style></head><body>
<h1>Day 21 | 從 VCF 到研究審閱封包</h1><div class="intro">QC → Fixture 註釋 → Evidence / Human Review</div>
<div class="warn"><strong>SYNTHETIC EDUCATIONAL DEMO</strong>:所有資料人工合成,非 GRCh38、非 AlphaGenome API 分數,未進行臨床判定。OFFLINE_ONLY。</div>
<div class="stats"><div class="stat"><div class="num">{report['candidate_count']}</div><div class="label">ALT 候選</div></div>
<div class="stat"><div class="num">{report['selected_count']}</div><div class="label">SELECTED</div></div>
<div class="stat"><div class="num">{report['review_required_qc_count']}</div><div class="label">QC REVIEW_REQUIRED</div></div>
<div class="stat"><div class="num">{report['rejected_count']}</div><div class="label">REJECTED</div></div></div>
<section><h2>第一站|QC 全部候選(保留失敗理由)</h2><p>順序:{col(' → '.join(report['qc_order']))};每筆記錄第一個決定性原因。</p>
<table><thead><tr><th>Variant</th><th>QC Status</th><th>Reason</th><th>QUAL</th></tr></thead><tbody>{qc}</tbody></table></section>
<section><h2>第二站|可供人工審閱的候選封包</h2><p>只有 SELECTED 進入此表;但所有八筆仍保留在 QC 表。缺分數記為 null,不填零。</p>
<table><thead><tr><th>Variant</th><th>Artificial Score</th><th>Score Status</th><th>Review Status</th></tr></thead><tbody>{packets}</tbody></table></section>
<footer>Evidence: SYNTHETIC_FIXTURE_ONLY · assembly: {col(report['assembly'])} · atlas_query_performed: false · real_api_calls_this_run: 0 · 研究用教學,非醫療建議。</footer>
</body></html>'''
def hash_file(path):
return hashlib.sha256(Path(path).read_bytes()).hexdigest()
def execute_offline(vcf_path, reference_path, regions_path, scores_path, output_dir):
paths = {"vcf": Path(vcf_path), "reference": Path(reference_path),
"regions": Path(regions_path), "scores": Path(scores_path)}
data = {key: path.read_text(encoding="utf-8") for key, path in paths.items()}
report = build_review_packet(data["vcf"], load_toy_reference(data["reference"]),
parse_toy_bed(data["regions"]), load_toy_scores(data["scores"]))
report["input_provenance"] = {key: {"filename": path.name, "sha256": hash_file(path)}
for key, path in paths.items()}
folder = Path(output_dir)
folder.mkdir(parents=True, exist_ok=True)
report_path = folder / "day21_review_packet.json"
html_path = folder / "day21_review_panel.html"
report_path.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
html_path.write_text(render_html(report), encoding="utf-8")
return {"status": "PASS", "mode": "OFFLINE_ONLY",
"candidate_count": report["candidate_count"],
"selected_count": report["selected_count"],
"review_required_qc_count": report["review_required_qc_count"],
"rejected_count": report["rejected_count"],
"missing_score_count": report["missing_score_count"],
"json_file": str(report_path), "html_file": str(html_path),
"real_api_calls_this_run": 0}
def main(argv=None):
parser = argparse.ArgumentParser(description="Offline synthetic VCF to research review packet")
parser.add_argument("--mode", choices=["offline"], required=True, help="LIVE intentionally unavailable")
parser.add_argument("--vcf", default="day21_synthetic.vcf")
parser.add_argument("--reference", default="day21_toy_reference.json")
parser.add_argument("--regions", default="day21_synthetic_regions.bed")
parser.add_argument("--scores", default="day21_synthetic_scores.json")
parser.add_argument("--output-dir", default="day21_output")
args = parser.parse_args(argv)
summary = execute_offline(args.vcf, args.reference, args.regions, args.scores, args.output_dir)
print(json.dumps(summary, sort_keys=True, ensure_ascii=False))
if __name__ == "__main__":
main()
如果系統沒有 unzip,不用安裝任何套件,可以利用 Python 自帶的 ZIP 模組解壓。確認 ZIP 已下載到 Windows Downloads,執行:
cd /mnt/c/Users/user/Downloads
mkdir -p ~/alphagenome-day21-qa
python3.12 -m zipfile -e AlphaGenome_Day21_VCF_Review_Packet_API_Bridge_Author_Review_Pack.zip ~/alphagenome-day21-qa
cd ~/alphagenome-day21-qa
ls -l day21_offline_vcf_review_lab.py test_day21_offline_vcf_review_lab.py day21_synthetic.vcf day21_synthetic_regions.bed day21_toy_reference.json day21_synthetic_scores.json
python3.12 -m unittest -v test_day21_offline_vcf_review_lab
python3.12 day21_offline_vcf_review_lab.py --mode offline --vcf day21_synthetic.vcf --reference day21_toy_reference.json --regions day21_synthetic_regions.bed --scores day21_synthetic_scores.json --output-dir day21_output
如果你已經把六個必要檔案分別下載在 Downloads,也可以照 Day 20 的成功做法,使用 cp 複製到 ~/alphagenome-day21-qa/,不需要使用 ZIP。
我在此執行環境以 Python 3.13.5 執行,得到下面的實測 stdout(你的 WSL Python 3.12 尚待驗收):
{"candidate_count": 8, "html_file": "day21_output/day21_review_panel.html", "json_file": "day21_output/day21_review_packet.json", "missing_score_count": 1, "mode": "OFFLINE_ONLY", "real_api_calls_this_run": 0, "rejected_count": 3, "review_required_qc_count": 2, "selected_count": 3, "status": "PASS"}
程式將生成 day21_output/day21_review_packet.json 和 day21_output/day21_review_panel.html。前者供機器與研究團隊複查,後者可以直接用瀏覽器開啟,不需要後端伺服器。
| 人工候選 | QC Status | 第一個決策原因 |
|---|---|---|
chr1:100:A>C |
SELECTED |
TEACHING_QC_PASS |
chr1:110:A>G |
SELECTED |
TEACHING_QC_PASS |
chr1:110:A>C |
SELECTED |
TEACHING_QC_PASS |
chr1:120:A>T |
REJECTED |
OUTSIDE_TEACHING_BED |
chr1:130:A>G |
REJECTED |
FILTER_NOT_PASS |
chr1:140:G>T |
REJECTED |
OUTSIDE_TEACHING_BED |
chr1:150:A>AT |
REVIEW_REQUIRED |
INDEL_NOT_NORMALIZED |
chr1:160:A>T |
REVIEW_REQUIRED |
MISSING_QUAL |
注意兩個「REVIEW」的意思: QC 表中的兩筆 REVIEW_REQUIRED 是因 indel/缺 QUAL,尚未通過本例門檻;而 Review Packet 裡三筆 SELECTED 的 review_status=REVIEW_REQUIRED 指的是即使資料通過 QC,仍需要人類專家審閱。兩者不是同一種計數。
| 候選 | 人工分數 | 註釋狀態 | 人工審閱 |
|---|---|---|---|
chr1:100:A>C |
0.42 | SYNTHETIC_SCORE |
REVIEW_REQUIRED |
chr1:110:A>G |
−0.17 | SYNTHETIC_SCORE |
REVIEW_REQUIRED |
chr1:110:A>C |
null |
MISSING_SCORE |
REVIEW_REQUIRED |
分數的正負不具有罹病風險解釋,null 也不等於 0。 所有資料皆為人工教學值。
Day 17 的 Reboot 歷史 Atlas 產物已證明:當時 SDK 0.9.0 曾成功執行 query_variant(),取得 AVI 與特徵欄位。但 Day 21 不能直接拿人工 chr1、人工 200 個 A 的參考序列去宣稱那是真正的 GRCh38 查詢。
未來在獲得授權後,要新增真實 Atlas adapter 時,至少有五個關卡:
SELECTED 候選轉成有明確來源的 Atlas 查詢請求,並確認安裝版 SDK 的函式與 scorer 參數。HISTORICAL_LIVE、CURRENT_LIVE 或 SYNTHETIC_FIXTURE,不可混成一個來源。這次沒有提供 LIVE adapter 的可執行功能。 CLI 只允許 --mode offline,以免誤觸金鑰或錯把人工分數稱為真實 AVI。即使電腦裡已有 API Key,這支程式也不會主動使用它。SDK 0.9.0 的歷史成功不等於 0.10.0 的現行 LIVE 相容性已驗證。
POS−1 才能和 BED 比較。3 SELECTED / 2 QC REVIEW_REQUIRED / 3 REJECTED。chr1:120、chr1:140 為什麼先被 BED 排除。null,而非 0。SYNTHETIC EDUCATIONAL DEMO。 這是一個最小化、僅八欄、無 genotype 的教學 VCF parser;無法代替完整 VCF/多等位基因 normalization/真實 FASTA 檢查/clinically validated variant prioritization。QUAL>=20 是本篇自行設定的教學門檻,非 AlphaGenome 規定或通用臨床篩選標準。BED 只用 VCF POS 錨點做練習,不能套用到真實複雜 indel。
輸入來源的 SHA-256 是檔案完整性線索,不證明資料來自可信實驗。這份程式不會觸及個人基因資料、真實 Atlas/Model API,也不會產生 ACMG/AMP 分類。原生 Reboot API 回應未完整留存,因此歷史 Atlas 數值是另一層獨立的衍生證據,不與本篇合成 score 併成同一次實驗。
明天 Day 22,我們會把今天的 QC 再細分:專門檢查 FILTER、QUAL、REF 與 BED 的順序與邊界,讓每一個候選變異留下能複查的品質理由。